/*
  author Sylvain Bertrand <sylvain.bertrand@gmail.com>
  Protected by linux GNU GPLv2
  Copyright 2012-2014
*/
#include <linux/pci.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/firmware.h>
#include <linux/delay.h>

#include <alga/rng_mng.h>
#include <uapi/alga/pixel_fmts.h>
#include <alga/timing.h>
#include <alga/amd/atombios/atb.h>
#include <uapi/alga/amd/dce6/dce6.h>

#include "mc.h"
#include "rlc.h"
#include "ih.h"
#include "fence.h"
#include "ring.h"
#include "dmas.h"
#include "ba.h"
#include "cps.h"
#include "gpu.h"
#include "drv.h"

#include "ucode.h"

#include "regs.h"

#include "smc_tbls.h"

#define UCODE_START 0x10000

#define TAHITI_FW_SZ	0xf458
#define PITCAIRN_FW_SZ	0xe9f4
#define VERDE_FW_SZ	0xebe4
#define OLAND_FW_SZ	0xe7b4

MODULE_FIRMWARE("radeon/TAHITI_smc.bin");
MODULE_FIRMWARE("radeon/PITCAIRN_smc.bin");
MODULE_FIRMWARE("radeon/VERDE_smc.bin");
MODULE_FIRMWARE("radeon/OLAND_smc.bin");

void smc_auto_increment_ena(struct pci_dev *dev)
{
	u32 smc_access_ctl;

	smc_access_ctl = rr32(dev, SMC_ACCESS_CTL);
	smc_access_ctl |= SAC_AUTO_INCREMENT;
	wr32(dev, smc_access_ctl, SMC_ACCESS_CTL);
}

void smc_auto_increment_dis(struct pci_dev *dev)
{
	u32 smc_access_ctl;

	smc_access_ctl = rr32(dev, SMC_ACCESS_CTL);
	smc_access_ctl &= ~SAC_AUTO_INCREMENT;
	wr32(dev, smc_access_ctl, SMC_ACCESS_CTL);
}

u32 smc_r32(struct pci_dev *dev, u32 addr)
{
	wr32(dev, addr, SMC_IDX);
	/* XXX: the xRBM does big endian reads from smc ram */
	return rr32(dev, SMC_DATA); 
}

void smc_w32(struct pci_dev *dev, u32 val, u32 addr)
{
	wr32(dev, addr, SMC_IDX);
	/* XXX: the xRBM does big endian writes to smc ram */
	wr32(dev, val, SMC_DATA);
}

#define SMC_IS_RUNNING		1
#define SMC_IS_NOT_RUNNING	0
u8 smc_is_running(struct pci_dev *dev)
{
	u32 reset;
	u32 clk;

	reset = smc_r32(dev, SMC_SYSCON_RESET_CTL);
	clk = smc_r32(dev, SMC_SYSCON_CLK_CTL);

	if (!(reset & SSRC_RST_REG) && !(clk & SSCC_CK_DIS))
		return SMC_IS_RUNNING;
	return SMC_IS_NOT_RUNNING;
}
#undef SMC_IS_RUNNING
#undef SMC_IS_NOT_RUNNING

void smc_reset(struct pci_dev *dev)
{
	u32 smc_syscon_reset_ctl;

	/* XXX: why read gpu cfg regs??? weird */
	rr32(dev, CB_CGTT_SCLK_CTL);
	rr32(dev, CB_CGTT_SCLK_CTL);
	rr32(dev, CB_CGTT_SCLK_CTL);
	rr32(dev, CB_CGTT_SCLK_CTL);

	smc_syscon_reset_ctl = smc_r32(dev, SMC_SYSCON_RESET_CTL);
	smc_syscon_reset_ctl |= SSRC_RST_REG;
	smc_w32(dev, smc_syscon_reset_ctl, SMC_SYSCON_RESET_CTL);
}

void smc_clk_stop(struct pci_dev *dev)
{
	u32 smc_syscon_clk_ctl;

	smc_syscon_clk_ctl = smc_r32(dev, SMC_SYSCON_CLK_CTL);
	smc_syscon_clk_ctl |= SSCC_CK_DIS;
	smc_w32(dev, smc_syscon_clk_ctl, SMC_SYSCON_CLK_CTL);
}

long smc_ucode_load(struct pci_dev *dev)
{
	struct dev_drv_data *dd;
	dd = pci_get_drvdata(dev);

	switch (dd->family) {
	case TAHITI:
		dd->smc_fw_dws = TAHITI_FW_SZ >> 2;
		break;
	case PITCAIRN:
		dd->smc_fw_dws = PITCAIRN_FW_SZ >> 2;
		break;
	case VERDE:
		dd->smc_fw_dws = VERDE_FW_SZ >> 2;
		break;
	case OLAND:
		dd->smc_fw_dws = OLAND_FW_SZ >> 2;
		break;
	}
	return ucode_load(dev, &(dd->smc_fw), "smc", dd->smc_fw_dws);
}

void smc_ucode_program(struct pci_dev *dev)
{
	struct dev_drv_data *dd;
	const __be32 *fw_data;
	u32 i;

	dd = pci_get_drvdata(dev);

	wr32(dev, UCODE_START, SMC_IDX);
	smc_auto_increment_ena(dev);

	fw_data = (const __be32 *)dd->smc_fw->data;
	for (i = 0; i < dd->smc_fw_dws; ++i)
		/*
		 * XXX: whatever the cpu architecture is, the dw value will be
		 * written on the pcie mmio memory as le32.
		 * ****BUT**** the xRBM will write that value as be32 in
		 * smc memory.
		 */
		wr32(dev, be32_to_cpup(fw_data++), SMC_DATA);

	smc_auto_increment_dis(dev);	
}

u8 smc_tbls_cur_arb_set_get(struct pci_dev *dev)
{
	u32 arb_dram_tbl_addr_addr;
	u32 arb_dram_tbl_addr;
	u32 be_dw;
	arb_dram_tbl_addr_addr = SMC_FW_HDR_START + SMC_FW_HDR_MC_ARB_TBL;

	arb_dram_tbl_addr = smc_r32(dev, arb_dram_tbl_addr_addr);

	/* see the layout of smc_mc_arb_dram_tbl, read is big endian */
	be_dw = smc_r32(dev, arb_dram_tbl_addr);
	return (u8)(be_dw >> 24);
}

static u32 sw_regs_start_addr_get(struct pci_dev *dev)
{
	u32 sw_regs_start_addr_addr;

	sw_regs_start_addr_addr = SMC_FW_HDR_START + SMC_FW_HDR_SW_REGS;
	return smc_r32(dev, sw_regs_start_addr_addr);
}

void smc_sw_wr32(struct pci_dev *dev, u32 val, u16 of)
{
	u32 sw_regs_start_addr;
	u32 reg_addr;

	sw_regs_start_addr = sw_regs_start_addr_get(dev);
	reg_addr = sw_regs_start_addr + of;
	smc_w32(dev, val, reg_addr);
}

u32 smc_sw_r32(struct pci_dev *dev, u16 of)
{
	u32 sw_regs_start_addr;
	u32 reg_addr;

	sw_regs_start_addr = sw_regs_start_addr_get(dev);
	reg_addr = sw_regs_start_addr + of;
	return smc_r32(dev, reg_addr);
}

void smc_memcpy_to(struct pci_dev *dev, u32 dest, u8 *src, u32 bytes_n)
{
	while (bytes_n >= 4) {
		/*
		 * XXX: whatever the cpu architecture is, the dw value will be
		 * written on the pcie mmio memory as le32.
		 * ****BUT**** the xRBM will write that value as be32 in
		 * smc memory.
		 */
		wr32(dev, dest, SMC_IDX);
		wr32(dev, be32_to_cpup((__be32*)src), SMC_DATA);

		src += 4;
		bytes_n -= 4;
		dest += 4;
	}

	/* the final dw */
	if (bytes_n > 0) {
		u32 last_dw_old;
		u32 last_dw_new;
		u8 extra_shift;

		wr32(dev, dest, SMC_IDX);
		last_dw_old = rr32(dev, SMC_DATA);

		extra_shift = 8 * (4 - bytes_n);

		last_dw_new = 0;
		while (bytes_n > 0) {
			last_dw_new = (last_dw_new << 8) + *src++;
			bytes_n--;
		}

		last_dw_new <<= extra_shift;

		last_dw_new |= (last_dw_old & ~((~0UL) << extra_shift));

		wr32(dev, dest, SMC_IDX);
		wr32(dev, last_dw_new, SMC_DATA);
	}
}

void smc_memcpy_from(struct pci_dev *dev, u8 *dest, u32 src, u32 bytes_n)
{
	while (bytes_n >= 4) {
		__be32 be32;
		u8 *be32p;

		/* the xRBM will read that value as be32 from smc memory */
		wr32(dev, src, SMC_IDX);
		be32 = (__be32)rr32(dev, SMC_DATA);
		be32p = (u8*)&be32;

		dest[0] = be32p[3];
		dest[1] = be32p[2];
		dest[2] = be32p[1];
		dest[3] = be32p[0];
		
		src += 4;
		bytes_n -= 4;
		dest += 4;
	}

	/* the final dw */
	if (bytes_n > 0) {
		__be32 be32_last;
		u8 *be32_lastp;
		u8 dest_byte_idx;
		u8 src_byte_idx;

		wr32(dev, src, SMC_IDX);
		be32_last = (__be32)rr32(dev, SMC_DATA);

		be32_lastp = (u8*)&be32_last;

		dest_byte_idx = 0;
		src_byte_idx = 3;
		while (bytes_n > 0) {
			dest[dest_byte_idx] = be32_lastp[src_byte_idx];
			++dest_byte_idx;
			src_byte_idx--;
			bytes_n--;
		}
	}
}

void smc_initial_jmp_setup(struct pci_dev *dev)
{
	static u8 jmp_instr[] = { 0x0e, 0x00, 0x40, 0x40 };

	smc_memcpy_to(dev, 0x0, &jmp_instr[0], sizeof(jmp_instr));
}

void smc_start(struct pci_dev *dev)
{
	u32 smc_syscon_reset_ctl;
	smc_syscon_reset_ctl = smc_r32(dev, SMC_SYSCON_RESET_CTL);

	smc_syscon_reset_ctl &= ~SSRC_RST_REG;
	smc_w32(dev, smc_syscon_reset_ctl, SMC_SYSCON_RESET_CTL);
}

void smc_clk_start(struct pci_dev *dev)
{
	u32 smc_syscon_clk_ctl;
	smc_syscon_clk_ctl = smc_r32(dev, SMC_SYSCON_CLK_CTL);

	smc_syscon_clk_ctl &= ~SSCC_CK_DIS;
	smc_w32(dev, smc_syscon_clk_ctl, SMC_SYSCON_CLK_CTL);
}

#define HALT_TIMEOUT_US 100000 /* 100 ms */
void smc_halt_wait(struct pci_dev *dev)
{
	u32 i;

	for (i = 0; i < HALT_TIMEOUT_US; ++i) {
		u32 smc_syscon_clk_ctl;

		smc_syscon_clk_ctl = smc_r32(dev, SMC_SYSCON_CLK_CTL);
		if ((smc_syscon_clk_ctl & SSCC_CKEN) == 0)
			return;
		udelay(1);
	}
	dev_warn(&dev->dev, "smc:halt timed out\n");
}

#define MSG_RESULT_OK		0x01
#define MSG_RESULT_FAILED	0xff
#define MSG_TIMEMOUT_US 100000 /* 100 ms */	
long smc_msg(struct pci_dev *dev, u16 msg)
{
	u32 i;
	u32 smc_msg_resp;

	wr32(dev, msg, SMC_MSG_SEND);

	for (i = 0; i < MSG_TIMEMOUT_US; ++i) {
		smc_msg_resp = rr32(dev, SMC_MSG_RESP);
		if (smc_msg_resp != 0)
			break;
		udelay(1);
	}

	if (smc_msg_resp == MSG_RESULT_OK)
		return 0;
	return -SI_ERR;
}

long smc_msg_param(struct pci_dev *dev, u16 msg, u32 param)
{
	wr32(dev, param, SMC_SCRATCH_0);
	return smc_msg(dev, msg);
}
